There is no spoon
Legacy:Power Core Volume Control Mutator
After a team's Power Core has been destroyed in an Onslaught game in UT2004 it explodes. After the initial explosion a repetative and loud electricity-short-circuit type sound is played.
This tutorial will show you the steps required to write a small mutator that will allow the volume of both the Power Core's initial explosion and the subsequent electricity sound to be adjusted to your preference.
Contents
- 1 How does the Power Core explosion actually work
- 2 Using the CheckReplacement() function
- 3 Controlling Emitters on the game clients
- 4 Catching the end of game
- 5 Adding the VolumeControlGameRule to the current game-rule set
- 6 Changing the Emitter sound volume
- 7 Making the power core volume changes configurable
- 8 Conclusion
How does the Power Core explosion actually work[edit]
In order to change the volume of the Power Core's end of game sounds we must first find the code that is responsible for playing the sound and explosion. Since the explosion relates directly to the Power Core it would seem sensible to look for it within the Onslaught.PowerCore class. Sure enough conveniently tucked away within the class is the method Legacy:Power Core Volume Control Mutator/PowerCoreDestroyed. The critical lines are shown below.
simulated function PowerCoreDestroyed() { if (Level.NetMode != NM_DedicatedServer) { // If this code is running on either a Listen server or a pure game client then // create the Power Core's explosion effect and sound. ExplosionEffect = spawn(class'ONSPowerCoreBreachEffect', self); } }
The most important thing to note about this code is that the ONSPowerCoreBreachEffect object that is ultimately responsible for playing the sounds we wish to modify is only spawn on game clients. This object is never created on a dedicated server.
An inspection of the ONSPowerCoreBreachEffect code shows us two things.
- It is subclassed directly from Emitter.
- All of it's properties are created as in-line objects within the defaultproperties sections.
Emitter objects by default have two really impotant properties.
RemoteRole=ROLE_None // Emitters are never sent across the network bNotOnDedServer=true // Emitters cannot be created on a dedicated server
So, in order to change the sound volumes via our mutator we need to locate the ONSPowerCoreBreachEffect object on each of the game clients and then adjust the Emitter's sound properties.
Using the CheckReplacement() function[edit]
When changing the properties of objects (like sound volume, ammo counts etc) you would typically catch their creation within the Mutator's CheckReplacement() function as follows:
class PowerCoreVolumeControl extends Mutator; function bool CheckReplacement(Actor Other, out byte bSuperRelevant) { if ( Other.IsA('ONSPowerNodeEnergySphere') { // Code to change volume goes in here. } }
Unfortunately the CheckReplacement() function is called on the server rather than any of the game's clients. As we discovered in the previous section the Emitter responsible for the Power Core's destruction effects is only ever spawned on the client. We need another way of doing it.
Controlling Emitters on the game clients[edit]
Since we cannot use our Mutator to trap the creation of the Power Core's breach effect we need to do the next best thing. That is, find it once it has been spawned on the client. In order to do this we create an instance of a new class that can be spawned by our Mutator and replicated to the game clients.
The code to adjust the Emitter's sound volume has been left out for now and instead we write out a log message.
class VolumeControlClient extends Actor; var bool bAdjustVolume; // False once the volume has been changed. // Every game tick we run through all of the current Emitter actors looking // for the ONSPowerCoreBreachEffect Emitter. Once we have found this we can // update its volume settings and stop checking. simulated event Tick( float DeltaTime ) { local ONSPowerCoreBreachEffect breachEffect; Super.Tick( DeltaTime ); if ( self.bAdjustVolume ) { // The foreach will examine every actor in the game!!! // This is an expensive (aka slow) operation. foreach AllActors(class'ONSPowerCoreBreachEffect', breachEffect) { self.bAdjustVolume = False; Log("We found a breach effect so update its sound"); } } } // Set the remote role so that the client will run the tick function when the // object is created there. // If bAlwaysRelevant is not set to true then the object is never replicated // to the game clients defaultproperties { bAdjustVolume=True RemoteRole=ROLE_SimulatedProxy bAlwaysRelevant=True }
Even though we only adjust our Emitters volume once as we set the bAdjustVolume property to false once we have adjusted the Emitters volume this is still a very inneficient way of doing things.
Running through all of the Actors in the game with every game tick is bad news. Since the Power Core's breach effect is only ever created when the game has been won we only really need to start looking for the Emitter after the game has ended.
Catching the end of game[edit]
Luckily for us UT2004 has a class specifically designed to allow the game ending conditions to be trapped and/or overidden. We need to create a subclass of GameRules. The GameRules class has a bunch of methods that are called whenever a player needs to start, scores a kill, etc. Most importantly it has a function that is called to see if the game has ended. Our GameRules subclass looks like this.
class VolumeControlGameRule extends GameRules; // If the game has ended then create the object responsible for setting the // Power Core's sound effects volume on the game-client machines. function bool CheckEndGame(PlayerReplicationInfo Winner, string Reason) { local VolumeControlClient volTracker; local bool bGameEnded; bGameEnded = Super.CheckEndGame(Winner, Reason); if ( bGameEnded ) { volTracker = Spawn(class'VolumeControlClient'); } return bGameEnded; }
As you can see from the above code it doesn't do very much. We are not interested in changing the rules of the game - merely detecting when it happens so we pass on the "game ended" check to any other game rules that are currently active. If the game has ended then we spawn the object (a VolumeControlClient) that will actually handle the volume update.
So how do we get our VolumeControlGameRule object into the set of rules the game is using? Easy - we get our Mutator class to add our GameRule object to it.
Adding the VolumeControlGameRule to the current game-rule set[edit]
Adding an instance of our VolumeControlGameRule class to the current game rules is very easy. We can do it by adding the following code to our Mutator.
// Announce to the log file that we have started and create the // GameRule object that will ultimately be responsible for spotting the end // of the game and creating the object that will actually update the sound // volumes we need. event PostBeginPlay() { local GameRules volumeRule; Super.PostBeginPlay(); volumeRule = spawn(class'VolumeControlGameRule'); if ( Level.Game.GameRulesModifiers == None ) { Level.Game.GameRulesModifiers = volumeRule; } else { Level.Game.GameRulesModifiers.AddGameRules(volumeRule); } // NOTE: Vampire mutator calls Destroy() here! }
- The only oddity about this function is that the Vampire mutator calls the Mutator's Destroy() function once it has spawned its game rule object. The implication of this is that game rules are sticky. By this I mean carried from one level to the next. However, in my testing I did not find that this was the case. It could be a minor performance enhancement - after all once our game rule object has been created the mutator object has no further use.
Changing the Emitter sound volume[edit]
At this point we have created our PowerCoreVolumeControl mutator class. This mutator exists solely for the purpose of adding a custom game rule (VolumeControlGameRule) to the current list of game rules. Our game rule class detects when the game has ended and creates a VolumeControlClient object. It is the VolumeControlClient object that is responsible for actually adjusting the volume of the emitter sound.
The Power Core's breach effect Emitter is actually a composition of 11 separate Emitters. All of these Emitters are stored in the Emitters property of the Emitter class. Adjusting the sound volume of the breach effect is simply a matter of looping through each of the emitters making up the breach effect and altering the volume of the sounds. The following code will do this.
// This is the function that actually updates the sound volume of the Emitter. // It loops through each of the Emitter's attached to the the BreachEffect // emitter and examines the sound attached to the "child" emitter updating as // required. // It is declared as simulated so it can be called from the simulated Tick() event. simulated function AdjustSoundVolume( Emitter breachEffect ) { local int i; for (i=0; i<breachEffect.Emitters.Length; i++) { // If the emitter we have found has a sound then check further. if ( breachEffect.Emitters[i].Sounds.Length > 0 ) { if ( i == 8 || i == 9 ) { // child emitters 8 and 9 are the electricity sound, so turn them off. breachEffect.Emitters[i].Sounds[0].Volume.Min = 0.0; breachEffect.Emitters[i].Sounds[0].Volume.Max = 0.0; } } } }
The for .. loop in the above code is a little redundant. However it does make it a little easier to allow the change in volume of the power core explosion and the electricity sound to be made configurable.
Making the power core volume changes configurable[edit]
In order to allow the user to configure the change in volume of the Power Core explosion and the electricity sound we need to update the signature of the class (to allow for the storage of the configuration information). We also need to add the configurable variables to the class definition. By the time you have done this your class should look something like that shown below.
class PowerCoreVolumeControl extends Mutator config (MutPowerCoreVolumeControl); // Values that will be stored within the MutPowerCoreColumeControl.ini file var() config float ExplosionVol; // The modifier for the explosion volume var() config float ElectricityVol; // The modifier for the electricity volume // Create the GameRule object that will catch the end of the game event PostBeginPlay() { local GameRules volumeRule; Super.PostBeginPlay(); volumeRule = spawn(class'VolumeControlGameRule'); if ( Level.Game.GameRulesModifiers == None ) { Level.Game.GameRulesModifiers = volumeRule; } else { Level.Game.GameRulesModifiers.AddGameRules(volumeRule); } } defaultproperties { ExplosionVol=1.0 ElectricityVol=0.1 }
However, this still doesn't make the attributes configurable via the GUI. In order to do this we can make use of some new functions added in UT2004. The function's are shown below:
// We need the following additional properties var localized string GIPropsDisplayText[2]; // Config property display names var localized string GIPropDescText[2]; // Config property descriptions // This function is called when the Mutator Configuration window is displayed // It is responsible for returning the widget information to be used to // configure the mutator. static function FillPlayInfo(PlayInfo PlayInfo) { Super.FillPlayInfo(PlayInfo); PlayInfo.AddSetting( default.RulesGroup, "ExplosionVol", class'PowerCoreVolumeControl'.default.GIPropsDisplayText[0], 0, 0, "Text", "8;0.0:1.5" ); PlayInfo.AddSetting( default.RulesGroup, "ElectricityVol", class'PowerCoreVolumeControl'.default.GIPropsDisplayText[1], 0, 0, "Text", "8;0.0:1.5" ); } // This function is called by the Mutator Configuration window when a // description of the configuration property is required. static event string GetDescriptionText(string PropName) { switch (PropName) { case "ExplosionVol": return class'PowerCoreVolumeControl'.default.GIPropDescText[0]; case "ElectricityVol": return class'PowerCoreVolumeControl'.default.GIPropDescText[1]; } return Super.GetDescriptionText(PropName); }
The functions are relatively self explanatory. Note that the second parameter to the PlayInfo.AddSetting() function call, and that used in the case statement must match the class property name being changed. In this case we have allowed the properties to be assigned a value between 0.0 and 1.5.
We still haven't quite finished yet though. We hard-coded the volume setting within the function responsible for adjusting the volume. We now need to go back and fix that. The final function is reproduced below.
simulated function AdjustSoundVolume( Emitter breachEffect ) { local int i; for (i=0; i<breachEffect.Emitters.Length; i++) { if ( breachEffect.Emitters[i].Sounds.Length > 0 ) { if ( i == 8 || i == 9 ) { breachEffect.Emitters[i].Sounds[0].Volume.Min *= class'PowerCoreVolumeControl'.default.ElectricityVol; breachEffect.Emitters[i].Sounds[0].Volume.Max *= class'PowerCoreVolumeControl'.default.ElectricityVol; } else { breachEffect.Emitters[i].Sounds[0].Volume.Min *= class'PowerCoreVolumeControl'.default.ExplosionVol; breachEffect.Emitters[i].Sounds[0].Volume.Max *= class'PowerCoreVolumeControl'.default.ExplosionVol; } } } }
You will notice that at no point do I ever replicate the server's Mutator configuration values to the clients. This is done deliberately to allow each client to configure their own preferred volume settings.
Conclusion[edit]
If you got this far you should (in theory) have a working mutator that you can use to alter the volume of the Power Core's breach effects. Not only that but you should have an appreciation of Emitter structure. You should also be aware that Emitters are purely game-client constructions. That is, they will never exist on a dedicated server. I hope you found the tutorial useful.
If you ever find a better way of catching an Emitter's creation than searching for it within the Tick() event I'd love to hear about it.
The compiled code and source code for this mutator is available here: http://www.snout-clan.co.uk/p.php?p=3065